"use strict";

var path = require('path'),
    C = require('./common.js'),
    _ = C._,
    S = C.S,
    E = C.Error,
    HttpCode = C.HttpCode,
    Base = C.Base;

var Router = Base.extend({
    /**
     * Express request object
     * @memberof Router#
     * @type {Request}
     */
    req: null,
    /**
     * Express response object
     * @memberof Router#
     * @type {Response}
     */
    res: null,
    /**
     * Target controller abslute path
     * @memberof Router#
     * @type {string}
     */
    controllerPath: null,
    /**
     * Action to dispatch
     * @memberof Router#
     * @type {string}
     */
    action: null,

    /**
     * The base class of routers.
     * @constructs Router
     * @param {string} mount - The base route to mount this router
     * @param {WebModule} webModule - The web module instance
     * @param {Request} req - Express request object
     * @param {Response} res - Express response object
     * @param {object} info
     */
    constructor: function(mount, webModule, req, res, info) {
        var self = this;

        this.req = req;
        this.res = res;
        this.controllerPath =  info.controllerPath;
        this.action = info.action;

        this.extend({
            /**
             * Get the owner module
             * @memberof Router#
             * @returns {WebModule}
             */
            getModule: function () {
                return webModule;
            },

            /**
             * Return the base route or construct a relative url to the web root
             * @memberof Router#
             * @param relativeUrl
             * @returns {*}
             */
            route: function (relativeUrl) {
                return relativeUrl ? C.urlJoin(mount, relativeUrl) : mount;
            },

            /**
             * Handle error in controller action
             * @memberof Router#
             * @param {string|Error} error - Error object or error message
             * @param {boolean} [json=false] - As JSON or not
             * @returns {*}
             */
            handleError: function (error, json) {
                return Router.onError(webModule, req, res, error, json);
            },

            /**
             * Forward to another action.
             * @memberof Router#
             * @param {string} controllerPath - Controller path
             * @param {string} action - Action name
             * @param {*} extra - Extra parameter
             * @returns {boolean} Forwarded or not
             */
            forward: function (controllerPath, action, extra) {
                if (controllerPath.indexOf('..') >= 0) {
                    throw new C.Error.UsageError('Invalid controller path: ' + controllerPath);
                }

                var absControllerPath = webModule.toAbsPath(controllerPath + '.js');
                var controller = require(absControllerPath);
                if (controller[action]) {

                    self.controllerPath = controllerPath;
                    self.action = action;
                    if (self.view) {
                        self.view = controllerPath + '_' + action;
                        self.model = {};
                    }

                    controller[action](self, extra);
                    return true;
                }

                return false;
            }
        });

        if (ENV === 'development') {
            webModule.log('debug', 'Router created.', {
                path: req.path,
                attachedModule: webModule.name,
                target: this.controllerPath + '.' + this.action
            });
        }
    },

    /**
     * Get the application's root url
     * @memberof Router#
     * @returns {string}
     */
    getAppUrl: function () {
        return this.req.protocol + "://" + this.req.get('host');
    },

    /**
     * Get current landing url
     * @memberof Router#
     * @returns {string}
     */
    getCurrentUrl: function () {
        return this.req.protocol + "://" + this.req.get('host') + this.req.url;
    },

    /**
     * Convert a relative url to a full url
     * @memberof Router#
     * @returns {string}
     */
    toFullUrl: function (relativeToRoot) {
        return this.req.protocol + "://" + C.urlJoin(this.req.get('host'), relativeToRoot);
    },

    /**
     * Get a redirect url by parameters
     * @memberof Router#
     * @param {string} url - Absolute url to redirect
     * @param {object} [query] - Extra query parameters
     * @param {object} [data] - Extra cached data
     * @param {string} [keyOfData] - Key name of the extra cached data
     */
    prepareRedirectUrl: function (url, query, data, keyOfData) {
        query = query || {};

        if (data) {
            var objId = C.H.cacheObject(data);

            keyOfData = keyOfData || 'msg';
            query[keyOfData] = objId;
        }

        if (!_.isEmpty(query)) {
            url = C.extendQuery(url, query);
        }

        return url;
    },

    /**
     * Redirect to an endpoint within this module
     * @memberof Router#
     * @param {string} relativeUrl - Relative url to this module
     * @param {object} [params] - Extra parameters
     * @param {object} [params.query] - Extra query parameters
     * @param {object} [params.data] - Extra cached data
     * @param {string} [params.keyOfData] - Key name of the extra cached data
     * @param {int} [params.httpCode=302] - HTTP code of the redirection
     */
    redirectInModule: function (relativeUrl, params) {
        this.redirectWithData(this.route(relativeUrl), params.query, params.data, params.keyOfData, params.httpCode);
    },

    /**
     * Redirect to an absolute url
     * @memberof Router#
     * @param {string} url - Absolute url to redirect
     * @param {object} [query] - Extra query parameters
     * @param {object} [data] - Extra cached data
     * @param {string} [keyOfData] - Key name of the extra cached data
     * @param {int} [httpCode=302] - HTTP code of the redirection
     */
    redirectWithData: function (url, query, data, keyOfData, httpCode) {
        url = this.prepareRedirectUrl(url, query, data, keyOfData);

        this.redirect(url, httpCode);
    },

    /**
     * Redirect to an absolute url
     * @memberof Router#
     * @param {string} url - Absolute url to redirect
     * @param {int} [httpCode=302] - HTTP code of the redirection
     */
    redirect: function (url, httpCode) {
        httpCode = httpCode || HttpCode.HTTP_FOUND;

        C.H.syslog('verbose', httpCode.toString() + ' Redirection: ' + url);

        this.res.redirect(httpCode, url);
    },

    /**
     * Send JSON response to the client
     * @memberof Router#
     * @param {object} [data] - Data returned as JSON
     * @param {int} [code=200] - HTTP code of the response
     */
    sendJson: function (data, code) {
        code = code || 200;
        this.res.json(code, data);
    }
}, {
    //static members
    getMiddlewares: function (webModule, route, options, cb) {

        var middlewares = webModule.getMiddlewaresChain();

        if (!C._.isEmpty(options)) {
            var middlewareLib = require('./features/middlewares.js');

            return C.async.eachSeries(C._.keys(options), function (name, createMiddlwareComplete) {
                middlewareLib.createMiddleware(webModule, route, name, options[name], function (err, middleware) {
                    if (err) return createMiddlwareComplete(err);

                    middlewares.push(middleware);
                    createMiddlwareComplete();
                });
            }, function (err) {
                if (err) return cb(err);

                cb(null, middlewares);
            });
        }

        cb(null, middlewares);
    },

    dispatch: function (req, res) {
        var router = req.router;
        var controllerPath = router.getModule().toAbsPath(router.controllerPath + '.js');

        if (controllerPath.indexOf('..') >= 0) {
            return router.handleError(C.HttpCode.HTTP_BAD_REQUEST);
        }

        try {
            var controller = require(controllerPath);

            var actioner = controller[router.action];
            if (typeof actioner !== 'function') {
                if (typeof controller._fallback === 'function') {
                    actioner = controller._fallback;
                } else {
                    return router.handleError(C.HttpCode.HTTP_NOT_FOUND);
                }
            }

            var params = [router].concat(router.params || []);
            actioner.apply(null, params);
        } catch (error) {
            router.handleError(error);
        }
    },

    onError: function (webModule, req, res, error, json) {
        var code = _.isNumber(error)
            ? error
            : (error.httpCode || (error.code === 'MODULE_NOT_FOUND' ? HttpCode.HTTP_NOT_FOUND : HttpCode.HTTP_INTERNAL_SERVER_ERROR));

        Router.logError(code, error, req);

        if (webModule.errorController) {
            if (webModule.errorController(req, res, error, code, json)) {
                return
            }
        }

        if (json) {
            var data = error.error ? _.omit(error, 'httpCode') : { error: webModule.app.__('nodeframe/http', code.toString()) };
            res.json(code, data);
        } else {
            res.send(code, error.message ? error.message : webModule.app.__('nodeframe/http', code.toString()));
        }
    },

    logError: function (code, error, req) {
        if (code >= 500 || ENV === 'development') {
            var message = 'HTTP ' + code + ' ' + req.originalUrl + ' from [' + req.ip + ']\n';

            if (error.stack) {
                message += error.stack;

                C.H.syslog('error', message);
            } else {
                message += C.H.callstack(3);
                if (_.isObject(error)) {
                    C.H.syslog('error', message, error);
                } else {
                    C.H.syslog('error', message);
                }
            }
        } else {
            C.H.syslog('warn', 'HTTP ' + code + ' ' + req.originalUrl);
        }
    }
});

var RouterWithView = Router.extend({
    /**
     * View path
     * @memberof RouterWithView#
     * @type {string}
     */
    view: null,

    /**
     * View model
     * @memberof RouterWithView#
     * @type {object}
     */
    model: {},

    /**
     * The base class of routers with renderable view.
     * @constructs RouterWithView
     * @mixes Router
     * @param {string} mount - The base route to mount this router
     * @param {WebModule} webModule - The web module instance
     * @param {Request} req - Express request object
     * @param {Response} res - Express response object
     * @param {object} info
     */
    constructor: function(mount, webModule, req, res, info) {
        this.base(mount, webModule, req, res, info);

        var self = this;

        this.view = info.view;

        var _viewBase = path.join(webModule.path, 'views');

        this.extend({
            /**
             * Render the HTTP response.
             * @memberof RouterWithView#
             * @param {string} [view] - The alternative view path
             */
            renderResponse: function(view) {
                if (view) self.view = view;

                var data = self.model;

                data.public = webModule.toPublicPath;
                data.env = ENV;
                data.path = req.url;
                data.route = self.route;
                data.route.url = self.toFullUrl(req.url);
                data.route.base = self.route();
                data.route.rootUrl = self.getAppUrl();

                self.res.render(path.join(_viewBase, self.view), data);
            },

            /**
             * Invoke a business logic action
             * @memberof RouterWithView#
             * @param {string} businessMethod
             * @returns {object}
             */
            invoke: function (businessMethod) {
                var pos = businessMethod.lastIndexOf('.');
                if (pos < 0) {
                    self.handleError({ httpCode: C.Error.HTTP_INTERNAL_SERVER_ERROR, message: 'Invalid action: ' + businessMethod });
                }

                var controller = businessMethod.substr(0, pos);
                var action = businessMethod.substr(pos + 1);

                var bl = require(webModule.toAbsPath(controller) + '.js');

                return bl[action](self);
            },

            /**
             * Invoke a business logic action and append the result to view model
             * @memberof RouterWithView#
             * @param {string} businessMethod
             * @param {string} key
             */
            loadModel: function (key, businessMethod) {
                self.appendViewData(key, self.invoke(businessMethod));
            }
        });
    },

    /**
     * Append a key-value pair into the view model
     * @memberof RouterWithView#
     * @param {string} key
     * @param {*} value
     */
    appendViewData: function (key, value) {
        if (_.isObject(key) && typeof value === 'undefined') {
            this.model = _.extend(this.model || {}, key);
        } else if (_.isString(key)) {
            this.model = this.model || {};
            this.model[key] = value;
        } else {
            throw new E.UsageError('Expected key-value pair or collection in arguments.');
        }
    }
});

exports.Router = Router;
exports.RouterWithView = RouterWithView;